﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Ckknight.StateMachine
{
    /// <summary>
    /// Provides an immutable factory that can generate instances of <see cref="StateMachine&lt;TState, TTrigger, TModel&gt;"/>
    /// </summary>
    /// <typeparam name="TState">The type used to represent the state of the machine.</typeparam>
    /// <typeparam name="TTrigger">The type used to represent triggers which cause transitions within the machine.</typeparam>
    /// <typeparam name="TModel">The type which the machine will carry with it and can be used to store information or react differently.</typeparam>
    public class StateMachineFactory<TState, TTrigger, TModel>
    {
        private readonly FrozenDictionary<TState, StateMachine<TState, TTrigger, TModel>.StateData> _states;
        private readonly IEqualityComparer<TState> _stateComparer;
        private readonly IEqualityComparer<TTrigger> _triggerComparer;
        private readonly Action<StateMachine<TState, TTrigger, TModel>.Transition> _onUnhandledTrigger;
        private readonly FrozenDictionary<TTrigger, Type[]> _triggerParameterTypes;

        internal StateMachineFactory(
            FrozenDictionary<TState, StateMachine<TState, TTrigger, TModel>.StateData> states,
            IEqualityComparer<TState> stateComparer,
            IEqualityComparer<TTrigger> triggerComparer,
            Action<StateMachine<TState, TTrigger, TModel>.Transition> onUnhandledTrigger,
            FrozenDictionary<TTrigger, Type[]> triggerParameterTypes)
        {
            _states = states;
            _stateComparer = stateComparer;
            _triggerComparer = triggerComparer;
            _onUnhandledTrigger = onUnhandledTrigger;
            _triggerParameterTypes = triggerParameterTypes;
        }

        /// <summary>
        /// Generate a new <see cref="StateMachine&lt;TState, TTrigger, TModel&gt;"/> with the provided <paramref name="initialState"/>.
        /// 
        /// This is condiered to be thread-safe as long as no further configuration of the builder occurs.
        /// </summary>
        /// <remarks>
        /// This will attach null as its model.
        /// </remarks>
        /// <param name="initialState">The starting state of the machine.</param>
        /// <returns>The generated state machine</returns>
        /// <exception cref="System.InvalidOperationException">
        /// Thrown when <typeparamref name="TModel"/> is not <see name="System.Object"/>.
        /// </exception>
        public StateMachine<TState, TTrigger, TModel> Generate(TState initialState)
        {
            if (typeof(TModel) != typeof(object))
            {
                throw new InvalidOperationException("Cannot generate without providing a model. Pass in null explicitly if that was your intention.");
            }

            return Generate(default(TModel), initialState);
        }

        /// <summary>
        /// Generate a new <see cref="StateMachine&lt;TState, TTrigger, TModel&gt;"/> with the provided <paramref name="initialState"/> and attached <paramref name="model"/>.
        /// 
        /// This is condiered to be thread-safe as long as no further configuration of the builder occurs.
        /// </summary>
        /// <remarks>
        /// This will attach null as its model.
        /// </remarks>
        /// <param name="model">The model to attach to this state machine.</param>
        /// <param name="initialState">The starting state of the machine.</param>
        /// <returns>The generated state machine</returns>
        public StateMachine<TState, TTrigger, TModel> Generate(TModel model, TState initialState)
        {
            var holder = new[] { initialState };
            return Generate(model, () => holder[0], newState => holder[0] = newState);
        }

        /// <summary>
        /// Generate a new <see cref="StateMachine&lt;TState, TTrigger, TModel&gt;"/> with functions to get and set the state.
        /// 
        /// This is condiered to be thread-safe as long as no further configuration of the builder occurs.
        /// </summary>
        /// <param name="getState">A function to get the current state.</param>
        /// <param name="setState">A function to set the current state.</param>
        /// <returns>The generated state machine</returns>
        /// <exception cref="System.InvalidOperationException">
        /// Thrown when <typeparamref name="TModel"/> is not <see name="System.Object"/>.
        /// </exception>
        public StateMachine<TState, TTrigger, TModel> Generate(Func<TState> getState, Action<TState> setState)
        {
            if (typeof(TModel) != typeof(object))
            {
                throw new InvalidOperationException("Cannot generate without providing a model. Pass in null explicitly if that was your intention.");
            }

            return Generate(default(TModel), getState, setState);
        }

        /// <summary>
        /// Generate a new <see cref="StateMachine&lt;TState, TTrigger, TModel&gt;"/> with functions to get and set the state and attached <paramref name="model"/>.
        /// 
        /// This is condiered to be thread-safe as long as no further configuration of the builder occurs.
        /// </summary>
        /// <param name="model">The model to attach to this state machine.</param>
        /// <param name="getState">A function to get the current state.</param>
        /// <param name="setState">A function to set the current state.</param>
        /// <returns>The generated state machine</returns>
        /// <exception cref="System.ArgumentNullException">
        /// Thrown when either <paramref name="getState"/> or <paramref name="setState"/> is null.
        /// </exception>
        public StateMachine<TState, TTrigger, TModel> Generate(TModel model, Func<TState> getState, Action<TState> setState)
        {
            if (getState == null)
            {
                throw new ArgumentNullException("getState");
            }
            else if (setState == null)
            {
                throw new ArgumentNullException("setState");
            }

            return new StateMachine<TState, TTrigger, TModel>(
                model,
                getState,
                setState,
                _states,
                _stateComparer,
                _triggerComparer,
                _onUnhandledTrigger,
                _triggerParameterTypes);
        }
    }
}
